/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package meteocal.weather;

import java.io.Serializable;
import meteocal.exception.UnavailableForecastException;
import meteocal.exception.CityNotFoundException;
import java.util.Calendar;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Named;
import javax.inject.Singleton;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonValue;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.MediaType;
import meteocal.entity.Event;
import meteocal.utility.DateManager;
import static meteocal.weather.Weather.*;

/**
 * This class retrieves weather forecasts provided by http://openweathermap.org
 * @author andrea
 */
@Named(value = "weather forecasts provider")
@Singleton
public class ForecastProvider implements Serializable{
    
    //API for weather forecast  by city name, provided by http://openweathermap.org
    private static final String DETAILED_FORECAST = "http://api.openweathermap.org/data/2.5/forecast?q=";
    private static final String DAILY_FORECAST = "http://api.openweathermap.org/data/2.5/forecast/daily?q=";
    
    //API parameters
    private static final String APIKEY = "&APPID=cabef366cab855c45d19bf3930b72db9";
    private static final String METRIC_UNITS = "&units=metric";
    private static final String DAYS_NUMBER = "&cnt=";
    private static final String JSON_MODE = "&mode=json";
    private static final String XML_MODE = "&mode=xml";
    
    private static final int MAX_DETAILED = 5;
    private static final int MAX_DAILY = 16;
    
    private Client client;
    
    @PostConstruct
    private void init(){
        this.client = ClientBuilder.newClient();
    }
    
    @PreDestroy
    private void drop(){
        this.client.close();
    }
    
    /**
     * Retrieves the weather forecast in the @param city on @param date.
     * Forecasts are provided in detail if the requested date is in 5 days from
     * the current date, or daily up to 15 days
     * @param city
     * @param date
     * @return the weather enum value
     * @throws CityNotFoundException if the given city is not supported by the provider
     * @throws UnavailableForecastException if the date of interest is further than 15 days 
     */
    public Weather getForecast(String city, Calendar date) throws UnavailableForecastException, CityNotFoundException {        
        
        if(!isAvailable(date))
            throw new UnavailableForecastException();        
        try{
            if(DateManager.getDayOffset(date) >= MAX_DETAILED)
                return getDailyForecast(city, date);
            
            return getDetailedForecast(city, date);
        }
        catch(CityNotFoundException | UnavailableForecastException e){
            throw e;
        }
    }
    
    /**
     * Finds an alternative date to the given one in order to reschedule an event
     * @param event
     * @return the new date if there are available forecasts, null otherwise
     */
    public Calendar findAlternative(Event event){
        
        try {
            String city = event.getPlace();
            Calendar date = event.getStartTime();
            Weather weather = event.getBadWeather();
            Weather newWeather;
            JsonArray forecast = sendDailyRequest(city).getJsonArray("list");
            for (JsonValue json : forecast) {
                JsonObject current = (JsonObject) json;
                newWeather = getWeather(current.getJsonArray("weather").getJsonObject(0)
                        .getInt("id"));
                long newDateLong = current.getJsonNumber("dt").longValue();
                Calendar newDate = DateManager.buildFromUnixTime(newDateLong);
                if(newWeather.isBetterThan(weather) && DateManager.isAfterToday(newDate)){
                    return newDate;
                }
            }
            
            return null;
        } catch (CityNotFoundException ex) {
            Logger.getLogger(ForecastProvider.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
        
    }
    
    /**
     * Retrieves the daily forecast in specific date and city
     * @param city
     * @param date 
     * @return weather enum
     * @throws CityNotFoundException if the specified place isn't supported by 
     * the provider.
     */
    private Weather getDailyForecast(String city, Calendar date) throws CityNotFoundException {
        
        JsonObject jsonWeather = sendDailyRequest(city);
        String cod = jsonWeather.getString("cod");
        if(cod.equals("404")){
            throw new CityNotFoundException();
        }
        
        int wCode = jsonWeather.getJsonArray("list")
                .getJsonObject(DateManager.getDayOffset(date))
                .getJsonArray("weather").getJsonObject(0)
                .getInt("id");
        
        return getWeather(wCode);
        
    }

    /**
     * Retrieves the detailed forecast in specific date and city
     * @param city
     * @param date
     * @return weather enum
     * @throws CityNotFoundException if the specified place isn't supported by 
     * the provider.
     * @throws UnavailableForecastException if there are no forecasts for the given
     * date. Tipically this should happen if the event is already past.
     */
    private Weather getDetailedForecast(String city, Calendar date) throws CityNotFoundException, UnavailableForecastException {
        
        JsonObject jsonWeather = sendDetailedRequest(city);
        String cod = jsonWeather.getString("cod");
        if(cod.equals("404")){
            throw new CityNotFoundException();
        }
        
        int wCode = 800;
        try{
            wCode = jsonWeather.getJsonArray("list")
                    .getJsonObject(DateManager.getHourOffset(date, jsonWeather.getJsonArray("list")))
                    .getJsonArray("weather").getJsonObject(0)
                    .getInt("id");
        }
        catch(ArrayIndexOutOfBoundsException e){
            throw new UnavailableForecastException();
        }
        
        return getWeather(wCode);
        
    }
    
    /**
     * Contacts OpenWeather provider to get a daily weather forecast for the
     * next 16 days
     * @param city
     * @return the JsonObject containing the forecast
     * @throws CityNotFoundException if the specified place isn't supported by 
     * the provider.
     */
    private JsonObject sendDailyRequest(String city) throws CityNotFoundException{
        
        String replace = city.replaceAll(" ", "+");
        String request = DAILY_FORECAST + replace
                + DAYS_NUMBER + MAX_DAILY
                + METRIC_UNITS
                + JSON_MODE
                + APIKEY;
        try{
            JsonObject json = client.target(request)
                    .request(MediaType.APPLICATION_JSON_TYPE)
                    .get(JsonObject.class);
            return json;
        }
        catch(Exception e){
            throw new CityNotFoundException();
        }
        
    }
    
    /**
     * Contacts OpenWeather provider to get a detailed weather forecast for the
     * next 5 days
     * @param city
     * @return the JsonObject containing the forecast
     * @throws CityNotFoundException if the specified place isn't supported by 
     * the provider.
     */
    private JsonObject sendDetailedRequest(String city) throws CityNotFoundException{
        
        String replace = city.replaceAll(" ", "+");
        String request = DETAILED_FORECAST + replace
                + DAYS_NUMBER + MAX_DETAILED
                + METRIC_UNITS
                + JSON_MODE
                + APIKEY;
        
        try{
            JsonObject json = client.target(request)
                    .request(MediaType.APPLICATION_JSON_TYPE)
                    .get(JsonObject.class);
            return json;
        }
        catch(Exception e){
            throw new CityNotFoundException();
        }
        
    }
    
    /**
     * Checks if there are available forecasts for the date of interest
     * @param date
     * @return true if date is in 15 days from the current date, false otherwise
     */
    private boolean isAvailable(Calendar date){
        return DateManager.getDayOffset(date) < MAX_DAILY;
    }
    
    /**
     * Converts the @param code provided by http://openweathermap.org into
     * the corresponding value of Weather enum
     * @param code
     * @return the corresponding Weather instance
     */
    private Weather getWeather(int code) {
        
        if(code < 300)
            return THUNDERSTORM;
        if(code < 600)
            return RAINY;
        if(code < 700)
            return SNOWY;
        if (code == 800)
            return SUNNY;
        if (code > 800 && code < 900)
            return CLOUDY;
        
        return STAY_HOME;
        
    }   
    
}
